#!/Library/Frameworks/Python.framework/Versions/Current/bin/python

"""
This is a Python wsgi/cgi implementation of the "export as image"
feature for amCharts.  It produces 256 color PNG images using the skimpyGimpy
library.  This implementation is extremely portable (pure python)
but produces strange images sometimes and also can be fairly slow.

It should be a trivial matter for someone who knows PIL to modify
the implementation to use the PIL C-extension modules to generate
more beautiful true color images more quickly.  This is left as an
exercize.

To use this cgi script install it at

   http://server.name.here/cgi-bin/getImage.cgi

and then in your amCharts settings include

   <settings>
       ...
       <export_as_image>  <file>/cgi-bin/getImage.cgi</file> </export_as_image>
       ...
   </settings>
   
for all possible values of /cgi-bin/getImage.cgi.
"""

import cgi

def imagedict(text):
    query_dict = cgi.parse_qs(text)
    return imagedictFromQueryDictionary(query_dict)

def imagedictFromQueryDictionary(query_dict):
    result = {}
    for (n,Lv) in query_dict.items():
        #pr "processing", (n,Lv)
        if n!="rows" and n.startswith("r"):
            assert len(Lv)==1, "don't understand value "+repr((n,Lv))
            [v] = Lv
            #pr "row", (n,v)
            rownum = int(n[1:])
            colnum = 0
            srow = v.split(",")
            for detail in srow:
                sdetail = detail.split(":")
                ndetail = len(sdetail)
                if ndetail==1:
                    [hexcolor] = sdetail
                    runlength = 1
                elif ndetail==2:
                    [hexcolor, runstring] = sdetail
                    runlength = int(runstring)
                else:
                    raise ValueError, "don't know what to do with detail "+repr(detail)
                for dx in xrange(runlength):
                    #pr (colnum+dx, -rownum), parseColor(hexcolor)
                    result[ (colnum+dx, -rownum) ] = parseColor(hexcolor)
                colnum += runlength
            #raise "debug stop"
        else:
            #pr "ignoring", (n, Lv)
            pass
    return histogramReduce(result)

def histogramReduce(imagedictionary):
    histogram = {}
    its = imagedictionary.items()
    for (p, c) in its:
        histogram[c] = histogram.get(c,0) + 1
    hist = [ (count, color) for (color, count) in histogram.items() ]
    hist.sort()
    hist.reverse()
    winners = hist[:240]
    losers = hist[240:]
    # add up to 16 reference colors to the color map
    colorMap = {}
    # 4 greys
    for x in (32,64,128,192):
        c = (x,x,x)
        colorMap[ c ] = c
    # 8 primaries
    for r in [0,255]:
        for b in [0,255]:
            for g in [0,255]:
                c = (r,b,g)
                colorMap[c] = c
    # xxxx 4 more?
    #pr "winners are", winners
    for (count, color) in winners:
        colorMap[color] = color
    winningColors = colorMap.keys()
    for (count, loser) in losers:
        nearestColor = nearestDistance = None
        for winner in winningColors:
            dist = colordistance(winner, loser)
            if nearestColor is None or dist<nearestDistance:
                nearestColor = winner
                nearestDistance = dist
        colorMap[loser] = nearestColor
    result = {}
    for (p,c) in its:
        result[p] = colorMap[c]
    return result

def colordistance(c1, c2):
    (r1,g1,b1) = c1
    (r2,g2,b2) = c2
    return (r1-r2)**2+(g1-g2)**2+(b1-b2)**2

def pngTextFromDict(dictionary):
    from skimpyGimpy import KiPNG
    colorDict = {}
    indexDict = {}
    count = 1
    its = dictionary.items()
    for (p,c) in its:
        if indexDict.get(c) is None:
            indexDict[c] = count
            colorDict[count] = c
            count += 1
    mappedDictionary = {}
    for (p,c) in its:
        mappedDictionary[p] = indexDict[c]
    #pr "colorDict", colorDict
    return KiPNG.DictToPNG(mappedDictionary, outfile=None, colorDict=colorDict, transparentIndex=None)

def downsampleColors(colors):
    "return old-->new where there are fewer than 256 colors"
    done = False
    map = {}
    mask = 1
    for c in colors:
        map[c] = c
    while not done:
        vmap = {}
        for c in map.values():
            vmap[c] = 1
        if len(vmap)<256:
            done = True
        else:
            #pr "downsample using mask", mask
            newmap = {}
            for (oldcolor, newcolor) in map.items():
                newnewcolor = [ bitRound(byte, mask) for byte in oldcolor ]
                newmap[oldcolor] = tuple(newnewcolor)
            map = newmap
        mask = mask*2+1
    return map
        
def parseColor(hexcolor):
    # pad with 0s on the left if needed
    lcolor = len(hexcolor)
    if lcolor<6:
        hexcolor = ("0"*(6-lcolor))+hexcolor
    cr = hexcolor[0:2]
    cg = hexcolor[2:4]
    cb = hexcolor[4:6]
    result = [ bitRound(int(c, 16), 7) for c in [cr,cg,cb] ]
    #result = [ int(c, 16) for c in [cr,cg,cb] ]
    return tuple(result)

def bitRound(byte, mask):
    remainder = byte&mask
    if remainder>mask/2:
        result = byte|mask
    else:
        result = byte-remainder-1
        if result<0:
            result = 0
    return result

# === wsgi interface

def application(env, start_response):
    "wsgi application interface for amcharts image conversion post"
    content_length_str = env.get("CONTENT_LENGTH")
    assert content_length_str, "image wsgi requires post data"
    content_length = int(content_length_str)
    inpt = env.get("wsgi.input")
    postdata = inpt.read(content_length)
    idict = imagedict(postdata)
    png = pngTextFromDict(idict)
    start_response("200 OK", [('Content-Type', 'image/png')])
    return [png]

# this if for WHIFF to recognize the application
__wsgi__ = application

# === cgi interface

def cgi_run():
    import wsgiref.handlers
    wsgiref.handlers.CGIHandler().run(application)

# === testing stuff

def testdict(filename="post.txt"):
    text = file(filename, "rb").read()
    return imagedict(text)

def test():
    d = testdict()
    v = {}
    for (a,b) in d.items():
        v[b] = 1
    #pr "values", v.keys()
    #pr
    #pr "test dict got", len(d), "keys and", len(v), "values"
    png = pngTextFromDict(d)
    fn = "/tmp/out.png"
    f = open(fn, "wb")
    f.write(png)
    f.close()
    print "dumped png data to", fn

if __name__=="__main__":
    test()
    # by default use the cgi interpretation
    #import cgitb
    #cgi_run()
